// ==UserScript==
// @name         Geocaching - Auto Decode Additional Hints
// @namespace    https://www.geocaching.com/
// @version      1.2
// @description  Automatically reveals and decodes Additional Hints on Geocaching cache pages.
// @match        https://www.geocaching.com/geocache/*
// @match        https://www.geocaching.com/seek/cache_details.aspx*
// @match        https://www.geocaching.com/seek/cdpf.aspx*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const HINT_ID = 'div_hint';
    const LINK_ID = 'ctl00_ContentBody_lnkDH';
    const HINT_BLOCK_ID = 'ctl00_ContentBody_hints';
    const KEY_ID = 'dk';

    function rot13Char(ch) {
        const code = ch.charCodeAt(0);

        if (code >= 65 && code <= 90) {
            return String.fromCharCode(((code - 65 + 13) % 26) + 65);
        }

        if (code >= 97 && code <= 122) {
            return String.fromCharCode(((code - 97 + 13) % 26) + 97);
        }

        return ch;
    }

    // Decode ROT13 text, but preserve HTML tags, HTML entities, and text inside [brackets].
    function convertROTStringWithBrackets(html) {
        let out = '';
        let decode = true;

        for (let i = 0; i < html.length; i++) {
            const ch = html.charAt(i);

            // Preserve HTML tags: <br>, <span>, etc.
            if (ch === '<') {
                const end = html.indexOf('>', i);
                if (end !== -1) {
                    out += html.slice(i, end + 1);
                    i = end;
                    continue;
                }
            }

            // Preserve HTML entities: &nbsp;, &quot;, etc.
            if (ch === '&') {
                const end = html.indexOf(';', i);
                if (end !== -1 && end - i <= 12) {
                    out += html.slice(i, end + 1);
                    i = end;
                    continue;
                }
            }

            // Preserve text inside brackets.
            if (ch === '[') {
                decode = false;
                out += ch;
                continue;
            }

            if (ch === ']') {
                decode = true;
                out += ch;
                continue;
            }

            out += decode ? rot13Char(ch) : ch;
        }

        return out;
    }

    function getHint() {
        return document.getElementById(HINT_ID);
    }

    function getDecryptLink() {
        return document.getElementById(LINK_ID);
    }

    function getHintBlock() {
        return document.getElementById(HINT_BLOCK_ID);
    }

    function revealHintContainer() {
        const hint = getHint();
        const block = getHintBlock();

        [hint, block].forEach(el => {
            if (!el) return;

            el.hidden = false;
            el.style.display = '';
            el.style.visibility = 'visible';
            el.style.opacity = '1';
        });
    }

    function removeDecryptKey() {
        const key = document.getElementById(KEY_ID);

        if (key && key.parentNode) {
            key.parentNode.removeChild(key);
        }

        const block = getHintBlock() || document;
        const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
        const nodes = [];

        while (walker.nextNode()) {
            nodes.push(walker.currentNode);
        }

        nodes.forEach(node => {
            if (/key\s*\(\s*decrypt\s*\)/i.test(node.nodeValue || '')) {
                node.nodeValue = '';
            }
        });
    }

    function linkStillSaysDecrypt(link) {
        if (!link) return false;

        const title = (link.getAttribute('title') || '').trim();
        const text = (link.textContent || '').trim();

        return /^decrypt$/i.test(title) || /^decrypt$/i.test(text);
    }

    function manuallyDecodeHint() {
        const hint = getHint();

        if (!hint || hint.dataset.autoHintDecoded === 'true') {
            return false;
        }

        const before = hint.innerHTML || '';

        if (!before.trim()) {
            return false;
        }

        hint.innerHTML = convertROTStringWithBrackets(before);
        hint.dataset.autoHintDecoded = 'true';

        const link = getDecryptLink();

        if (link) {
            link.setAttribute('title', 'Encrypt');
            link.textContent = 'Encrypt';
        }

        return true;
    }

    function clickBuiltInDecrypt() {
        const link = getDecryptLink();

        if (!link) {
            return false;
        }

        if (linkStillSaysDecrypt(link)) {
            try {
                link.click();
                return true;
            } catch (e) {
                return false;
            }
        }

        return false;
    }

    function decodeHints() {
        revealHintContainer();

        // First try Geocaching's built-in decrypt button.
        clickBuiltInDecrypt();

        // Then force decode if the built-in decrypt did not work.
        setTimeout(() => {
            revealHintContainer();

            const linkAfterClick = getDecryptLink();

            if (!linkAfterClick || linkStillSaysDecrypt(linkAfterClick)) {
                manuallyDecodeHint();
            }

            revealHintContainer();
            removeDecryptKey();
        }, 300);
    }

    function start() {
        let tries = 0;

        const timer = setInterval(() => {
            tries++;

            const hint = getHint();
            const link = getDecryptLink();

            if (hint || link) {
                decodeHints();
            }

            if ((hint && (hint.innerHTML || '').trim()) || tries > 60) {
                clearInterval(timer);
            }
        }, 250);
    }

    start();
})();